package com.googlecode.horncomb.orm.id;

import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

import javax.persistence.Entity;
import javax.persistence.Id;

import org.springframework.beans.BeanUtils;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import com.googlecode.horncomb.core.id.Identifiable;

public abstract class EntityIdUtil {
	private static Map<Class<?>, Field> idFieldCache = new HashMap<Class<?>, Field>();
	private static Map<Class<?>, PropertyDescriptor> idPropertyDescriptorCache = new HashMap<Class<?>, PropertyDescriptor>();
	private static Map<Class<?>, String> idNameCache = new HashMap<Class<?>, String>();

	public static void initIdLocationCacheIfNeed(Class<?> entityClass,
			String defaultIdName) {
		initIdLocationCache(entityClass, defaultIdName, false);
	}

	/**
	 * @param entityClass
	 * @param defaultIdName
	 * @param force
	 *            if force load whatever it's cached.
	 */
	public static void initIdLocationCache(Class<?> entityClass,
			String defaultIdName, boolean force) {
		Assert.notNull(entityClass);
		Field cachedIdField = getIdFieldFromCache(entityClass);
		PropertyDescriptor cachedIdPropertyDescriptor = getIdPropertyDescriptorFromCache(entityClass);
		String cachedIdName = getIdNameFromCache(entityClass);
		// force or not cached
		if (force
				|| ((cachedIdField == null || cachedIdPropertyDescriptor == null) && StringUtils
						.hasText(cachedIdName))) {
			Object idLoc = getIdLocation(entityClass, defaultIdName);
			idFieldCache.remove(entityClass);
			idPropertyDescriptorCache.remove(entityClass);
			idNameCache.remove(entityClass);
			if (idLoc instanceof Field) {
				Field idField = (Field) idLoc;
				putIdFieldIntoCache(entityClass, idField);
			} else if (idLoc instanceof PropertyDescriptor) {
				PropertyDescriptor idPropertyDescriptor = (PropertyDescriptor) idLoc;
				putIdPropertyDescriptorIntoCache(entityClass,
						idPropertyDescriptor);
			}
		}
	}

	public static Field getIdFieldFromCache(Class<?> entityClass) {
		return idFieldCache.get(entityClass);
	}

	/**
	 * cache field and validate newIdField is matched with cached property name.
	 * @param entityClass
	 * @param newIdField
	 */
	public static void putIdFieldIntoCache(Class<?> entityClass,
			Field newIdField) {
		Assert.notNull(entityClass);
		Assert.notNull(newIdField);
		String newIdName = newIdField.getName();
		String cachedIdName = getIdNameFromCache(entityClass);
		if (!StringUtils.hasText(cachedIdName)) {
			idNameCache.put(entityClass, newIdName);
		} else if (!cachedIdName.equals(newIdName)) {
			throw new IllegalStateException("new field[" + newIdName
					+ "] mismatch with cached id name[" + cachedIdName
					+ "] for type[" + entityClass.getName() + "]");
		}
		idFieldCache.put(entityClass, newIdField);
		idPropertyDescriptorCache.remove(entityClass);
	}

	public static PropertyDescriptor getIdPropertyDescriptorFromCache(
			Class<?> entityClass) {
		return idPropertyDescriptorCache.get(entityClass);
	}

	/**
	 * cache newIdPropertyDescriptor and validate newIdPropertyDescriptor is
	 * matched with cached property name.
	 * @param entityClass
	 * @param newIdPropertyDescriptor
	 */
	public static void putIdPropertyDescriptorIntoCache(Class<?> entityClass,
			PropertyDescriptor newIdPropertyDescriptor) {
		Assert.notNull(entityClass);
		Assert.notNull(newIdPropertyDescriptor);
		String newIdName = newIdPropertyDescriptor.getDisplayName();
		String cachedIdName = getIdNameFromCache(entityClass);
		if (!StringUtils.hasText(cachedIdName)) {
			idNameCache.put(entityClass, newIdName);
		} else if (!cachedIdName.equals(newIdName)) {
			throw new IllegalStateException("new property[" + newIdName
					+ "] mismatch with cached id name[" + cachedIdName
					+ "] for type[" + entityClass.getName() + "]");
		}
		idPropertyDescriptorCache.put(entityClass, newIdPropertyDescriptor);
		idFieldCache.remove(entityClass);
	}

	/**
	 * get
	 * @param entityClass
	 * @return
	 */
	public static String getIdNameFromCache(Class<?> entityClass) {
		return idNameCache.get(entityClass);
	}

	/**
	 * put the name of id into cache
	 * @param entityClass
	 * @param idName
	 */
	public static void putIdNameIntoCache(Class<?> entityClass, String idName) {
		Assert.notNull(entityClass);
		Assert.hasLength(idName);
		Field cachedIdField = getIdFieldFromCache(entityClass);
		PropertyDescriptor cachedIdPropertyDescriptor = getIdPropertyDescriptorFromCache(entityClass);
		if (cachedIdField == null && cachedIdPropertyDescriptor == null) {
			initIdLocationCacheIfNeed(entityClass, idName);
		} else if (cachedIdField != null) {
			// had cached field.
			// validate cached Field matched with new property name.
			if (!cachedIdField.getName().equals(idName))
				throw new IllegalStateException("new id name[" + idName
						+ "] mismatch with cached Field["
						+ cachedIdField.getName() + "] for type["
						+ entityClass.getName() + "]");
		} else if (cachedIdPropertyDescriptor != null) {
			// had cached property descriptor.
			// validate cached property descriptor matched with new property
			// name.
			if (!cachedIdPropertyDescriptor.getDisplayName().equals(idName))
				throw new IllegalStateException("new id name[" + idName
						+ "] mismatch with cached propertyDescritor["
						+ cachedIdPropertyDescriptor.getDisplayName()
						+ "] for type[" + entityClass.getName() + "]");
		}

	}

	/**
	 * locate the id.
	 * @param entityClass
	 * @param idName
	 *            the name of id field for candidate
	 * @return
	 */
	private static Object getIdLocation(Class<?> entityClass, String idName) {
		Assert.notNull(entityClass);
		Object result = null;

		if (Identifiable.class.isAssignableFrom(entityClass)) {
			result = BeanUtils.getPropertyDescriptor(entityClass, "id");
		}

		// find as Entity
		if (result == null && entityClass.isAnnotationPresent(Entity.class)) {
			for (PropertyDescriptor pd : BeanUtils
					.getPropertyDescriptors(entityClass)) {
				if (pd.getReadMethod().isAnnotationPresent(Id.class)) {
					result = pd;
					break;
				}
			}
			for (Field field : entityClass.getFields()) {
				if (field.isAnnotationPresent(Id.class)) {
					result = field;
					break;
				}
			}
		}

		if (result == null && StringUtils.hasText(idName)) {
			// find in property (get method)
			try {
				result = BeanUtils.getPropertyDescriptor(entityClass, idName);
			} catch (Exception ex) { // ignore
			}
		}

		if (result == null) {
			throw new NullPointerException("can not find id property in type["
					+ entityClass + "]");
		} else {
			return result;
		}
	}

	public static Serializable getIdValue(Object entity)
			throws IllegalArgumentException, IllegalAccessException,
			NoSuchMethodException, InvocationTargetException,
			InstantiationException {
		return getIdValue(entity, null);
	}

	public static Serializable getIdValue(Object entity, String defaultIdName)
			throws IllegalArgumentException, IllegalAccessException,
			NoSuchMethodException, InvocationTargetException,
			InstantiationException {
		initIdLocationCacheIfNeed(entity.getClass(), defaultIdName);
		return getIdValue(entity, getIdFieldFromCache(entity.getClass()),
				getIdPropertyDescriptorFromCache(entity.getClass()));
	}

	/**
	 * 根据实体对象获得这个实体的Id的值
	 * @param entity
	 * @param idField
	 * @param idPropertyDescriptor
	 * @return
	 * @throws NoSuchMethodException
	 * @throws IllegalAccessException
	 * @throws InvocationTargetException
	 * @throws InstantiationException
	 */
	public static Serializable getIdValue(Object entity, Field idField,
			PropertyDescriptor idPropertyDescriptor)
			throws NoSuchMethodException, IllegalAccessException,
			InvocationTargetException, InstantiationException {
		Assert.notNull(entity);
		// 判断是否是Entity接口类型
		if (entity instanceof Identifiable) {
			return ((Identifiable<?>) entity).getId();
		} else {
			Assert.isTrue(idField != null || idPropertyDescriptor != null,
					"idField or idPropertyDescriptor must not be null.");
			if (idField != null) {
				return (Serializable) idField.get(entity);
			} else {
				Method readMethod = idPropertyDescriptor.getReadMethod();
				if (!readMethod.isAccessible()) {
					readMethod.setAccessible(true);
				}
				return (Serializable) readMethod.invoke(entity);
			}
		}
	}

	public static void setIdValue(Object entity, Serializable idValue)
			throws IllegalArgumentException, IllegalAccessException,
			InvocationTargetException, NoSuchMethodException {
		setIdValue(entity, idValue, null);
	}

	public static void setIdValue(Object entity, Serializable idValue,
			String defaultIdName) throws IllegalArgumentException,
			IllegalAccessException, InvocationTargetException,
			NoSuchMethodException {
		initIdLocationCacheIfNeed(entity.getClass(), defaultIdName);
		setIdValue(entity, idValue, getIdFieldFromCache(entity.getClass()),
				getIdPropertyDescriptorFromCache(entity.getClass()));
	}

	/**
	 * 设置Id的值
	 * @param entity
	 * @param idValue
	 * @throws NoSuchMethodException
	 * @throws InvocationTargetException
	 * @throws IllegalAccessException
	 * @throws NoSuchMethodException
	 * @throws IllegalAccessException
	 * @throws InvocationTargetException
	 * @throws InstantiationException
	 */
	@SuppressWarnings("unchecked")
	public static void setIdValue(Object entity, Serializable idValue,
			Field idField, PropertyDescriptor idPropertyDescriptor)
			throws IllegalAccessException, InvocationTargetException,
			NoSuchMethodException {
		Assert.notNull(entity);
		if (entity instanceof Identifiable) {
			Identifiable e = (Identifiable) entity;
			e.setId(idValue);
		} else {
			Assert.isTrue(idField != null || idPropertyDescriptor != null,
					"idField or idPropertyDescriptor must not be null.");
			if (idField != null) {
				idField.set(entity, idValue);
			} else if (idPropertyDescriptor != null) {
				Assert.notNull(idPropertyDescriptor);
				Method writeMethod = idPropertyDescriptor.getWriteMethod();
				// if (!writeMethod.isAccessible()) {
				// writeMethod.setAccessible(true);
				// }
				writeMethod.invoke(entity, idValue);
			}
		}
	}

}
